<?php
class Preader2 {
	private $bodypq; //this is only body object
	private $title;
	private $bodydom;
	private $regs = array (
		"unlikelyCandidatesRe" => "/combx|comment|disqus|foot|header|menu|meta|nav|rss|shoutbox|sidebar|sponsor/i",
		"okMaybeItsACandidateRe" => "/and|article|body|column|main/i",
		"positiveRe" => "/article|body|content|entry|hentry|page|pagination|post|text/i",
		"negativeRe" => "/combx|comment|contact|foot|footer|footnote|link|media|meta|promo|related|scroll|shoutbox|sponsor|tags|widget/i",
		"divToPElementsRe" => "/<(a|blockquote|dl|div|img|ol|p|pre|table|ul)/i",
		"replaceBrsRe" => "/(<br[^>]*>[ \n\r\t]*){2,}/i",
		"replaceFontsRe" => "/<(\/?)font[^>]*>/i",
		"trimRe" => "/^[\s|　]+|[\s|　]+$/",
		"normalizeRe" => "/[\s|　]{2,}/",
		"killBreaksRe" => "/(<br\s*\/?>(\s|&nbsp;?)*){1,}/",
		"videoRe" => "/http:\/\/(www\.)?(youtube|vimeo)\.com/i",
		"annotationRe" => "/<!--.*?-->/i"
	);
	function __construct($bodyhtml, $title) {
		$this->bodypq = $bodyhtml;
		$this->title = $title;
		$this->bodydom = $this->bodypq->getDOMDocument();
		$this->removeScript();
		$this->removeStyle();
		$this->prepDocument();

		/* Build readability's DOM tree */
		$overlay = $this->bodydom -> createElement("div");
		$innerDiv = $this->bodydom -> createElement("div");
		$overlay->setAttribute('id',"readOverlay");
		$innerDiv->setAttribute('id' , "readInner");

		/* Apply user-selected styling */
		$readStyle="style-newspaper";
		$this->bodydom->getElementsByTagName('body')->item(0)->setAttribute('class',$readStyle);
		$overlay->setAttribute('class',$readStyle);
		$readMargin="margin-wide";
		$readSize="size-large";
		$overlay->setAttribute('class',$readMargin.' '.$readSize);
		
		// 过滤标题出来
		$articleTitle = $this -> getArticleTitle();
		$innerDiv-> appendChild($articleTitle);
		
		// 过滤内容出来
		$articleContent = $this->grabArticle();
		$innerDiv ->appendChild($articleContent);

		$overlay ->appendChild($overlay);
		$this->bodypq->html('');
		$body=$this->bodydom->getElementsByTagName('body')->item(0);
		$body->insertBefore($overlay, $body->firstChild);

		$htmlbody = $this->bodypq->html();

	}

	/**
	 * remove all the script tags
	 */
	public function removeScript() {
		$this->bodypq->find('script')->remove();
	}
	/**
	 * remove all the style tags
	 */
	public function removeStyle() {
		$this->bodypq->find('style')->remove();
	}
	/**
	 * prepare the html document for scrape it.
	 */
	public function prepDocument() {
		$html = preg_replace($this->regs['replaceBrsRe'], '</p><p>', $this->bodypq->html());
		$html = preg_replace($this->regs['replaceFontsRe'], '<$1span>', $html);
		$html = preg_replace($this->regs['annotationRe'], '', $html);
		$this->bodypq->html($html);
	}
	/**
	 * get the article title as an H1
	 */
	public function getArticleTitle() {
		$h=$this->bodydom->createElement('h1',$this->title);
		return $h;
	}
	/**
	 * grabArticle - Using a variety of metrics (content score,
	 * classname, element types), find the content that is most likely
	 * to be the stuff a user wants to read. Then return it wrapped up
	 * in a div.
	 */
	public function grabArticle() {
		$divs = $this->bodypq->find('div');
		$divsdom = $this->bodydom->getElementsByTagName('div');
		for ($i = 0; $i < $divs->size(); $i++) {
			$d = $this->bodypq->find('div:eq(' . $i . ')');

			//当没有匹配到divToPElementsRe中的节点时，则将其中内容直接转成P标签内容。否则遍历，将文字节点内容用P包含起来
			if (!preg_match($this->regs['divToPElementsRe'], $d->html())) {
				$d->replaceWith('<p>' . $d->html() . '</p>');
			} else {
				$d = $divsdom->item($i);
				if ($d->hasChildNodes()) {
					$child = $d->childNodes;
					if ($child->nodeType == 3) {
						$n = $html->createElement('p', $child->nodeValue);
						$n->setAttribute('style', 'display:inline');
						$n->setAttribute('class', 'readability-styled');
						$child->parentNode->replaceChild($n, $child);
						//$d->tagName='p';
					}
				}
			}
		}
		/**
		 * Loop through all paragraphs, and assign a score to them based
		 * on how content-y they look. Then add their score to their
		 * parent node.
		 * 
		 * A score is determined by things like number of commas, class
		 * names, etc. Maybe eventually link density.
		 */
		$ps = $this->bodypq->find('p');
		$psdom = $this->bodydom->getElementsByTagName('p');

		$candidates = array ();

		for ($i = 0; $i < $psdom->length; $i++) {
			$p = $psdom->item($i); //get one P element
			$parent = $p->parentNode;

			$innerText = $this->getInnerText($p);

			/*
			 * one english character's length is 1, and an utf-8's chiness character's length is 3
			 */
			if (strlen($innerText) < 25)
				continue;

			if ($parent->getAttribute('contentscore') == '') {
				$this->initialNode($parent);
				array_push($candidates, $parent);
			}

			/*
			* Initialize readability data for the grandparent. if
			* (typeof grandParentNode.readability == 'undefined') {
			* readability.initializeNode(grandParentNode);
			* candidates.push(grandParentNode); }
			*/
			$contentScore = 0;

			/* Add a point for the paragraph itself as a base. */
			$contentScore++;

			/* Add points for any commas within this paragraph */
			$contentScore += count(split(',', $innerText));

			/*
			 * For every 100 characters in this paragraph, add another
			 * point. Up to 3 points.
			 */
			$contentScore += min(floor(strlen($innerText) / 100), 3);

			/* Add the score to the parent. The grandparent gets half. */
			$parentsc = $parent->getAttribute('contentscore') + $contentScore;
			$parent->setAttribute('contentscore', $parentsc);

			// grandParentNode.readability.contentScore += contentScore/
			// 2;
		}

		/**
		 * After we've calculated scores, loop through all of the
		 * possible candidate nodes we found and find the one with the
		 * highest score.
		 */
		$topCandidate = null;
		for ($i = 0, $il = count($candidates); $i < $il; $i++) {
			/**
			 * Scale the final candidates score based on link density.
			 * Good content should have a relatively small link density
			 * (5% or less) and be mostly unaffected by this operation.
			 */
			$candidates[$i]->setAttribute('contentscore', $candidates[$i]->getAttribute('contentscore') * (1 - $this->getLinkDensity($candidates[$i])));

			if (!$topCandidate || $candidates[$i]->getAttribute('contentscore') > $topCandidate->getAttribute('contentscore'))
				$topCandidate = $candidates[$i];
		}

		/**
		 * If we still have no top candidate, just use the body as a
		 * last resort. We also have to copy the body node so it is
		 * something we can modify.
		 */
		if ($topCandidate == null || strtoupper($topCandidate->tagName) == "BODY") {
			$topCandidate = $this->bodydom->createElement("div", $this->bodypq->html());
			$this->bodypq->html("");
			$this->bodydom->getElementsByTagName('body')->item(0)->appendChild($topCandidate);
			$this->initialNode($topCandidate);
		}

		/**
		 * Now that we have the top candidate, look through its siblings
		 * for content that might also be related. Things like
		 * preambles, content split by ads that we removed, etc.
		 */
		$articleContent = $this->bodydom->createElement('div');
		$articleContent->setAttribute('id', "readability-content");
		$siblingScoreThreshold = max(10, $topCandidate->getAttribute('contentscore') * 0.2);
		$siblingNodes = $topCandidate->parentNode->childNodes;
		for ($i = 0, $il = $siblingNodes->length; $i < $il; $i++) {
			$siblingNode = $siblingNodes->item($i);
			$append = false;

			if ($siblingNode === $topCandidate) {
				$append = true;
			}

			if ($siblingNode->getAttribute('contentscore') != '' && $siblingNode->getAttribute('contentscore') >= $siblingScoreThreshold) {
				$append = true;
			}

			if ($siblingNode->nodeName == "P") {
				$linkDensity = $this->getLinkDensity($siblingNode);
				$nodeContent = $this->getInnerText($siblingNode);
				$nodeLength = strlen($nodeContent);

				if ($nodeLength > 80 && $linkDensity < 0.25) {
					$append = true;
				} else
					if ($nodeLength < 80 && $linkDensity == 0 && preg_match("/\.( |$) /", $nodeContent)) {
						$append = true;
					}
			}

			if ($append) {

				/*
				 * Append sibling and subtract from our list because it
				 * removes the node when you append to another node
				 */
				$articleContent->appendChild($siblingNode);
				$i--;
				$il--;
			}
		}

		/**
		 * So we have all of the content that we need. Now we clean it
		 * up for presentation.
		 */
		$this->prepArticle($articleContent);

		return $articleContent;

	}
	/**
	 * Prepare the article node for display. Clean out any inline
	 * styles, iframes, forms, strip extraneous
	 * <p>
	 * tags, etc.
	 * 
	 * @param Element
	 * @return void
	 */
	public function prepArticle($articleContent) {
		$this->cleanStyles($articleContent);
		$this->killBreaks($articleContent);

		/* Clean out junk from the article content */
		$this->clean($articleContent, "form");
		$this->clean($articleContent, "object");
		$this->clean($articleContent, "h1");
		/***************************************************************
		 * If there is only one h2, they are probably using it as a
		 * header and not a subheader, so remove it since we already
		 * have a header.
		 **************************************************************/
		if ($articleContent->getElementsByTagName('h2')->length == 1)
			$this->clean($articleContent, "h2");
		$this->clean($articleContent, "iframe");

		$this->cleanHeaders($articleContent);

		/*
		 * Do these last as the previous stuff may have removed junk
		 * that will affect these
		 */
		$this->cleanConditionally($articleContent, "table");
		$this->cleanConditionally($articleContent, "ul");
		$this->cleanConditionally($articleContent, "div");

		/* Remove extra paragraphs */
		$articleParagraphs = $articleContent->getElementsByTagName('p');
		for ($i = $articleParagraphs->length - 1; $i >= 0; $i--) {
			$cur = $articleParagraphs->item($i);
			$imgCount = $cur->getElementsByTagName('img')->length;
			$embedCount = $cur->getElementsByTagName('embed')->length;
			$objectCount = $cur->getElementsByTagName('object')->length;

			if ($imgCount == 0 && $embedCount == 0 && $objectCount == 0 && $this->getInnerText($cur, false) == '') {
				$cur->parentNode->removeChild($cur);
			}
		}

		try {
			$articleContent->nodeValue = preg_replace("/<br[^>]*>\s*<p/i", '<p', $articleContent->nodeValue);
		} catch (Exception $e) {
			dbg("Cleaning innerHTML of breaks failed. This is an IE strict-block-elements bug. Ignoring.");
		}
	}
	/**
	 * Clean an element of all tags of type "tag" if they look fishy.
	 * "Fishy" is an algorithm based on content length, classnames, link
	 * density, number of images & embeds, etc.
	 * 
	 * @return void
	 */
	public function cleanConditionally($elm, $tag) {
		$tagsList = $elm->getElementsByTagName($tag);
		$curTagsLength = $tagsList->length;

		/**
		 * Gather counts for other typical elements embedded within.
		 * Traverse backwards so we can remove nodes at the same time
		 * without effecting the traversal.
		 * 
		 * TODO: Consider taking into account original contentScore
		 * here.
		 */
		for ($i = $curTagsLength -1; $i >= 0; $i--) {
			$cur = $tagsList->item($i);
			$weight = $this->getClassWeight($cur);

			if ($weight < 0) {
				$cur->parentNode->removeChild($cur);
			} else
				if ($this->getCharCount($cur, ',') < 10) {
					/**
					 * If there are not very many commas, and the number of
					 * non-paragraph elements is more than paragraphs or
					 * other ominous signs, remove the element.
					 */

					$p = $cur->getElementsByTagName("p")->length;
					$img = $cur->getElementsByTagName("img")->length;
					$li = $cur->getElementsByTagName("li")->length - 100;
					$input = $cur->getElementsByTagName("input")->length;

					$embedCount = 0;
					$embeds = $cur->getElementsByTagName("embed");
					for ($ei = 0, $il = $embeds->length; $ei < $il; $ei++) {
						if (!preg_match($this->regs['videoRe'], $embeds->item($ei)->getAttribute('src'))) {
							$embedCount++;
						}
					}

					$linkDensity = $this->getLinkDensity($cur);
					$contentLength = strlen($this->getInnerText($cur));
					$toRemove = false;

					if ($img > $p) {
						$toRemove = true;
					} else
						if ($li > $p && $tag != "ul" && $tag != "ol") {
							$toRemove = true;
						} else
							if ($input > floor($p / 3)) {
								$toRemove = true;
							} else
								if ($contentLength < 25 && ($img == 0 || $img > 2)) {
									$toRemove = true;
								} else
									if ($weight < 25 && $linkDensity > .2) {
										$toRemove = true;
									} else
										if ($weight >= 25 && $linkDensity > .5) {
											$toRemove = true;
										} else
											if (($embedCount == 1 && $contentLength < 75) || $embedCount > 1) {
												$toRemove = true;
											}

					if ($toRemove) {
						$cur->parentNode->removeChild($cur);
					}
				}
		}
	}
	/**
	 * Get the number of times a string s appears in the node e.
	 * 
	 * @param Element
	 * @param string -
	 *            what to split on. Default is ","
	 * @return number (integer)
	 */
	public function getCharCount($elm, $s) {
		$s = $s || ",";
		return count(split($s, $this->getInnerText($elm)));
	}

	/**
	 * Clean out spurious headers from an Element. Checks things like
	 * classnames and link density.
	 * 
	 * @param Element
	 * @return void
	 */
	public function cleanHeaders($elm) {
		for ($headerIndex = 1; $headerIndex < 7; $headerIndex++) {
			$headers = $elm->getElementsByTagName('h' + $headerIndex);
			for ($i = $headers->length - 1; $i >= 0; $i--) {
				$h = $headers->item($i);
				if ($this->getClassWeight($h) < 0 || $this->getLinkDensity($h) > 0.33) {
					$h->parentNode->removeChild($h);
				}
			}
		}
	}

	/**
	 * Clean a node of all elements of type "tag". (Unless it's a
	 * youtube/vimeo video. People love movies.)
	 * 
	 * @param Element
	 * @param string
	 *            tag to clean
	 * @return void
	 */
	public function clean($elm, $tag) {
		$targetList = $elm->getElementsByTagName($tag);
		$isEmbed = ($tag == 'object' || $tag == 'embed');

		for ($y = $targetList->length - 1; $y >= 0; $y--) {
			/*
			 * Allow youtube and vimeo videos through as people usually
			 * want to see those.
			 */
			if (isEmbed && preg_match($this->regs['videoRe'], $targetList->item($y) . nodeValue)) {
				continue;
			}
			$t = $targetList->item($y);
			$t->parentNode->removeChild($t);
		}
	}

	/**
	 * Remove extraneous break tags from a node.
	 * 
	 * @param Element
	 * @return void
	 */
	public function killBreaks($elm) {
		try {
			$elm->nodeValue = preg_replace($this->regs['killBreaksRe'], '<br />', $elm->nodeValue);
		} catch (Exception $e) {
			//dbg("KillBreaks failed - this is an IE bug. Ignoring.");
		}
	}

	/**
	 * Remove the style attribute on every e and under. TODO: Test if
	 * getElementsByTagName(*) is faster.
	 * 
	 * @param Element
	 * @return void
	 */
	public function cleanStyles($elm) {
		if (!$elm) {
			return;
		}

		$cur = $elm->firstChild;

		// Remove any root styles, if we're able.
		if ($elm->getAttribute('class') != 'readability-styled')
			$elm->removeAttribute('style');

		// Go until there are no more child nodes
		while ($cur != null) {
			if ($cur->nodeType == 1) {
				// Remove style attribute(s) :
				if ($cur->getAttribute('class') != "readability-styled") {
					$cur->removeAttribute("style");
				}
				$this->cleanStyles($cur);
			}
			$cur = $cur->nextSibling;
		}
	}
	/**
	 * Get the density of links as a percentage of the content This is
	 * the amount of text that is inside a link divided by the total
	 * text in the node.
	 * 
	 * @param $elm : an dom element
	 * @return number (float)
	 */
	public function getLinkDensity($elm) {
		$links = $elm->getElementsByTagName("a");
		$textLength = strlen($this->getInnerText($elm));
		$linkLength = 0;
		for ($i = 0, $il = $links->length; $i < $il; $i++) {
			$linkLength += strlen($this->getInnerText($links->item($i)));
		}

		return $linkLength / $textLength;
	}

	/**
	 * Initialize a node with the readability object. Also checks the
	 * className/id for special names to add to its score.
	 * 
	 * @param Element
	 * @return void
	 */
	public function initialNode($elm) {
		$elm->setAttribute('contentscore', 0);

		switch (strtoupper($elm->tagName)) {
			case 'DIV' :
				$elm->setAttribute('contentscore', 5);
				break;

			case 'PRE' :
			case 'TD' :
			case 'BLOCKQUOTE' :
				$elm->setAttribute('contentscore', 3);
				break;

			case 'ADDRESS' :
			case 'OL' :
			case 'UL' :
			case 'DL' :
			case 'DD' :
			case 'DT' :
			case 'LI' :
			case 'FORM' :
				$elm->setAttribute('contentscore', -3);
				break;

			case 'H1' :
			case 'H2' :
			case 'H3' :
			case 'H4' :
			case 'H5' :
			case 'H6' :
			case 'TH' :
				$elm->setAttribute('contentscore', -5);
				break;

		}

		$sc = $elm->getAttribute('contentscore');
		$sc += $this->getClassWeight($elm);
		$elm->setAttribute('contentscore', $sc);
	}

	/**
	 * Get an elements class/id weight. Uses regular expressions to tell
	 * if this element looks good or bad.
	 * 
	 * @param $elm : a dom element
	 * @return number
	 */
	public function getClassWeight($elm) {
		$weight = 0;

		/* Look for a special classname */
		if (($cls = $elm->getAttribute('class')) != "") {

			if (preg_match($this->regs['negativeRe'], $cls)) {
				$weight -= 25;
			}
			if (preg_match($this->regs['positiveRe'], $cls)) {
				$weight += 25;
			}

		}

		/* Look for a special ID */
		if (($eid = $elm->getAttribute('id')) != "") {

			if (preg_match($this->regs['negativeRe'], $eid)) {
				$weight -= 25;
			}
			if (preg_match($this->regs['positiveRe'], $eid)) {
				$weight += 25;
			}
		}

		return $weight;
	}
	/**
	 * get the $elm's text content, exclude the tags
	 * the $elm is a dom element
	 */
	public function getInnerText($elm, $normalizeSpaces = true) {
		$text = preg_replace($this->regs['trimRe'], '', $elm->nodeValue);
		if ($normalizeSpaces)
			$text = preg_replace($this->regs['normalizeRe'], ' ', $text);
		return $text;
	}
	public function getHtml() {
		return $this->bodydom->saveHTML();
	}
}
?>